/*
 
(c) 2008 by Sławek Kruczek
slav.mir@gmail.com
 

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/


package analytics;

import java.util.List;
import java.util.ArrayList;
import java.util.Stack;
import java.util.EmptyStackException;

import analytics.util.*;




public class DefineableBinaryFunction implements BinaryFunction {
    
    private final String definition;
    
    private final UnaryFunctions functions = new UnaryFunctions();
    private final Constants constants = new Constants();
    
    private final List<RPNElement> executionList = new ArrayList<RPNElement>();
    
    
    public DefineableBinaryFunction(String definition) {
        this.definition = definition;
        
        // http://en.wikipedia.org/wiki/Shunting_yard_algorithm
        
        TokenExtractor tokenExtractor = new TokenExtractor(definition);
        Stack<RPNElement> buildStack = new Stack<RPNElement>();
        
        // to see if all the arguments are used up and there aren't too many functions - should equal 1 in the end
        int execStackDepth = 0;
        
        RPNElement previousElement = null;
        boolean negated = false;
        
        while(tokenExtractor.hasNext()) {
            String newToken = tokenExtractor.next();
            
            RPNElement newElement;
            
            // If it's a constant or a variable, put it on the execution list
            if((newElement = RPNConstant.parseConstant(newToken)) != null) {
                if(negated) {
                    newElement = RPNConstant.parseConstant("-" + newToken);
                    negated = false;
                }
                executionList.add(newElement);
                execStackDepth++;
            }
            else if(constants.get(newToken) != null) {
                double val = constants.get(newToken);
                executionList.add(new RPNConstant(negated ? -val : val));
                negated = false;
                execStackDepth++;
            }
            else if((newElement = RPNVariable.parseVariable(newToken)) != null) {
                if(negated) {
                    newElement = RPNNegatedVariable.parseVariable(newToken);
                    negated = false;
                }
                executionList.add(newElement);
                execStackDepth++;
            }
            // If it's a function, put it on the stack
            else if(functions.get(newToken) != null) {
                if(!negated) {
                    newElement = new RPNUnaryFunction(functions.get(newToken));
                }
                else {
                    newElement = new RPNNegatedUnaryFunction(functions.get(newToken));
                    negated = false;
                }
                buildStack.push(newElement);
                // exec stack depth is not changed
            }
            // Operators
            else if((newElement = RPNOperator.parseOperator(newToken)) != null) {
                
                if(negated) {
                    throw new IllegalArgumentException("Expression is incomplete.");
                }
                
                RPNOperator newOperator = (RPNOperator)newElement;
                switch(newOperator.getType()) {
                    
                    case SUB:
                         // determining whether "-" means negation or subtraction
                        if(previousElement == null || previousElement instanceof RPNOperator || (previousElement instanceof RPNMeta && ((RPNMeta)previousElement).getType() != RPNMeta.Type.R_PAREN)) {
                            negated = true;
                            break;

                        }
                        
                    case ADD:
                        while(buildStack.size() > 0 && buildStack.peek() instanceof RPNOperator) {
                            
                            executionList.add(buildStack.pop());
                            execStackDepth--;
                            
                        }
                        break;
                        
                    case MUL:
                    case DIV:
                        while(buildStack.size() > 0 && buildStack.peek() instanceof RPNOperator) {
                            
                            RPNOperator topElement = (RPNOperator)buildStack.peek();
                            if(topElement.getType() == RPNOperator.Type.ADD || topElement.getType() == RPNOperator.Type.SUB)
                                break;
                            
                            executionList.add(buildStack.pop());
                            execStackDepth--;
                            
                        }
                        break;
                        
                    case POW:
                        while(buildStack.size() > 0 && buildStack.peek() instanceof RPNOperator) {
                            
                            RPNOperator topElement = (RPNOperator)buildStack.peek();
                            if(topElement.getType() != RPNOperator.Type.POW)
                                break;
                            
                            executionList.add(buildStack.pop());
                            execStackDepth--;
                            
                        }
                        break;
                }
                
                if(!negated) {
                    buildStack.add(newElement);
                }
            }
            // parentheses, commas
            else if((newElement = RPNMeta.parseMeta(newToken)) != null) {
                
                RPNMeta newMeta = (RPNMeta)newElement;
                switch(newMeta.getType()) {
                    
                    case L_PAREN:
                        if(negated) {
                            buildStack.push(new RPNUnaryFunction(functions.get("neg")));
                            negated = false;
                        }
                        buildStack.push(newElement);
                        break;
                        
                    case R_PAREN:
                        if(negated) {
                            throw new IllegalArgumentException("Expression is incomplete.");
                        }
                        
                        try {
                            // pop all the operators and add them to the execution list
                            while(buildStack.peek() instanceof RPNOperator) {
                                executionList.add(buildStack.pop());
                                execStackDepth--;
                            }
                            
                            // there should be an opening paren on top of the stack, if there isn't the input is wrong
                            try {
                                RPNMeta topElement = (RPNMeta)buildStack.peek();
                                if(topElement.getType() == RPNMeta.Type.L_PAREN) {
                                    buildStack.pop();
                                }
                                else throw new IllegalArgumentException("Parentheses are not matching.");
                            }
                            catch(ClassCastException e) {
                                throw new IllegalArgumentException("Parentheses are not matching.");
                            }
                            
                            // if there is a function on top of the stack, add it to the execution list
                            if(buildStack.size() > 0 && buildStack.peek() instanceof RPNUnaryFunction) {
                                executionList.add(buildStack.pop());
                                // unary functions don't change execution stack's depth
                            }
                        }
                        catch(EmptyStackException e) {
                            throw new IllegalArgumentException("Parentheses are not matching.");
                        }
                        
                        break;
                        
                    case ARG_SEP:
                        // not implemented yet
                        break;
                }
            }
            else throw new IllegalArgumentException("Unknown symbol: " + newToken);
            
            previousElement = newElement;
        }
        
        // popping all remaining operators
        while(buildStack.size() > 0 && buildStack.peek() instanceof RPNOperator) {
            executionList.add(buildStack.pop());
            execStackDepth--;
        }
        
        if(buildStack.size() != 0) throw new IllegalArgumentException("Expression is incomplete.");
        

        if(execStackDepth > 1) {
            throw new IllegalArgumentException("Expression is incomplete. There are more arguments than functions.");
        }
        else if(execStackDepth < 1) {
            throw new IllegalArgumentException("Expression is incomplete.");
        }
        
        
    }

    public double eval(double x, double y) {
        RPNExecutionEnvironment env = new RPNExecutionEnvironment(x, y);
        
        for(RPNElement e: executionList) {
            e.exec(env);
        }
        
        assert env.getStackSize() == 1;
        return env.pop();
    }
    
    public String getDefinition() {
        return definition;
    }

}
